#ifndef __BVHACCELERATOR__
#define __BVHACCELERATOR__

//TODO fill here
#include "BoundingBox.h"
#include "GeometricObjects.h"
#include "Ray.h"
#include <vector>
#include <iostream>
#include <queue>
#include "Logger.h"
#include <sstream>
#include "Material.h"

#define BUCKET_SIZE 1
#define DEPTH_SIZE 12

namespace Engine
{
	using Engine::BoundingBox;
	using std::queue;
	using std::vector;

	enum PartitioningStyle
	{
		SAH,
		MIDDLE_POINT,
		HALF
	};

	struct PrimitiveBvhRepresentation
	{
	public:
		BoundingBox* primitiveBoundingBox;
		int index;
		Point3D boundingBoxCentroid;
		GeometricObjects* primitivePointer;

		PrimitiveBvhRepresentation()
		{
			primitiveBoundingBox = new BoundingBox();
			index = 0;
			boundingBoxCentroid = Point3D(0);
			primitivePointer = NULL;
		}

		// TODO fill with proper parameters
		PrimitiveBvhRepresentation(BoundingBox* p_Box, int& p_index, GeometricObjects* p_PrimitivePointer) : primitiveBoundingBox(p_Box), index(p_index), primitivePointer(p_PrimitivePointer)
		{
			boundingBoxCentroid = p_Box->GetCentroid();
		}
	};
	//TODO will contain bounding box 

	class BVHNode
	{
	public:

		bool childNode;
		BoundingBox* boundingBox;
		int splittingAxis;
		double splittingAxisValue;
		vector<GeometricObjects*> objects;
		Engine::BVHNode *leftChild;
		Engine::BVHNode *rightChild;

		BVHNode()
		{
			childNode = false;
			boundingBox = new BoundingBox();
		}

		BVHNode(Point3D p_minPoint, Point3D p_maxPoint)
		{
			boundingBox = new BoundingBox(p_minPoint, p_maxPoint);
		}

		BVHNode(vector<PrimitiveBvhRepresentation>::iterator p_first, vector<PrimitiveBvhRepresentation>::iterator p_last, PartitioningStyle p_style, int depth)
		{
			childNode = false;
			boundingBox = new BoundingBox();

			int amountOfObjects = p_last - p_first;

			//Logger::LogInfo("current node "+to_string(amountOfObjects));

			InitNode(p_first, p_last, p_style, depth);
		}

		void InitNode(vector<PrimitiveBvhRepresentation>::iterator first, vector<PrimitiveBvhRepresentation>::iterator last, PartitioningStyle style, int depth)
		{
			int size = last - first;
			childNode = size <= BUCKET_SIZE || depth >= DEPTH_SIZE;

			Point3D minTemp(INT_MAX);
			Point3D maxTemp(INT_MIN);

			for (vector<PrimitiveBvhRepresentation>::iterator iter = first; iter != last; ++iter)
			{
				boundingBox->Union((*iter).primitiveBoundingBox);

				minTemp.Min((*iter).boundingBoxCentroid);
				maxTemp.Max((*iter).boundingBoxCentroid);

				if (childNode)
				{
					objects.push_back((*iter).primitivePointer);
				}
			}

			if (!childNode)
			{
				//splittingAxis = GetLongestAxis();
				splittingAxis = GetLongestAxis(minTemp, maxTemp);
				splittingAxisValue = GetSplitOfAxis(splittingAxis, minTemp, maxTemp);

				if (style == MIDDLE_POINT)
				{
					auto lastFirst = std::partition(first, last,
						[this](PrimitiveBvhRepresentation const& x){return MiddlePointPartition(x); });

					leftChild = new BVHNode(first, lastFirst, style, ++depth);
					rightChild = new BVHNode(lastFirst, last, style, ++depth);
				}
			}
		}

		int GetLongestAxis(Point3D& minPoint, Point3D& maxPoint)
		//int GetLongestAxis()
		{
			Point3D diagVector =maxPoint - minPoint;

			if (diagVector.x > diagVector.y && diagVector.x > diagVector.z)
			{
				return 0;
			}
			else if (diagVector.y > diagVector.z)
			{
				return 1;
			}
			else
			{
				return 2;
			}
		}

		float GetSplitOfAxis(int& side,Point3D& minTemp, Point3D& maxTemp)
		{
			Point3D temp = (maxTemp - minTemp) / 2;
			Point3D diagVector = temp + minTemp;
			switch (side)
			{
			case 0:
				return diagVector.x;
			case 1:
				return diagVector.y;
			case 2:
				return diagVector.z;
			default:
				break;
			}
		}


		bool MiddlePointPartition(PrimitiveBvhRepresentation current)
		{
			if (splittingAxis == 0)
			{
				return current.boundingBoxCentroid.x <= splittingAxisValue;
			}
			else if (splittingAxis == 1)
			{
				return current.boundingBoxCentroid.y <= splittingAxisValue;
			}
			else
			{
				return current.boundingBoxCentroid.z <= splittingAxisValue;
			}
		}

	private:
	};
	
	class FlatBvhNode
	{
	public:
		BoundingBox * bbox;
		int leftChildIndex;
		int rightChildIndex;
		int myIndex;
		BVHNode *representingNode;
		bool childNode;
		
		FlatBvhNode(int index, BVHNode* currentNode)
		{
			bbox = currentNode->boundingBox;
			myIndex = index;
			representingNode = currentNode;
			if (currentNode->childNode)
			{
				childNode = true;
				
			}
			else
			{
				childNode = false;
			}
		}

	};
	
	
	class BVH 
	{
	public:
		BVHNode *rootNode;
		FlatBvhNode* flatNodes[100];
		bool m_flatBvh;

		BVH()
		{

		}

		//debugging purposes only
		BVH(Point3D p_minPoint, Point3D p_maxPoint)
		{
			rootNode = new BVHNode(p_minPoint, p_maxPoint);
		}

		BVH(vector<GeometricObjects*> sceneObjects, PartitioningStyle style =  MIDDLE_POINT, bool p_flatBvh = false)
		{
			TranslateObjects(sceneObjects);
			BuildTree(style);
			int index = 0;
			if (p_flatBvh)
			{
				m_flatBvh = p_flatBvh;
				
				FlattenBvh(rootNode, index);
			}
		}

		void TranslateObjects(vector<GeometricObjects*> sceneObjects)
		{
			for (int index = 0; index < sceneObjects.size(); index ++)
			{
				objectsRepresentation.push_back(PrimitiveBvhRepresentation(sceneObjects[index]->GetBoundingBox(), index, sceneObjects[index]));
			}
		}


		void BuildTree(PartitioningStyle style)
		{
			int depth = 0;
			rootNode = new BVHNode(objectsRepresentation.begin(), objectsRepresentation.end(), style, depth);
		}

		bool TestIntersection(Ray ray, ShadeRec& rec,double& tmin, bool p_shadowTest = false)
		{
			if (m_flatBvh)
			{
				return TestIntersectionFlatBvh(ray, rec, tmin,p_shadowTest);
			}
			else
			{
				return TestIntersectionNormal(ray, rec, p_shadowTest);
			}
		}

		bool TestIntersectionFlatBvh(Ray ray, ShadeRec& sr,double& tmin, bool p_shadowTest = false)
		{
			//TODO FIX HERE pass tmin
			//double tmin = INT_MAX;
			double tmax = INT_MIN;
			
			bool testIntersection = false;
			 
			queue<int> indexesToVisit;
			indexesToVisit.push(0);
			while (indexesToVisit.size() > 0)
			{
				int currentIndex = indexesToVisit.front();
				indexesToVisit.pop();

				FlatBvhNode* currentNode = flatNodes[currentIndex];

				if (currentNode->childNode)
				{
					if (currentNode->representingNode != NULL)
					{
						for (int index = 0; index < currentNode->representingNode->objects.size(); index++)
						{
							bool tempIntersection = currentNode->representingNode->objects[index]->hit(ray.o, ray.d, tmin, sr);
							if (p_shadowTest)
								return true;
							else if (tempIntersection && !testIntersection)
								testIntersection = true;
						}
					}
				}
				else
				{
					double tempMin = INT_MAX;
					double tempMax = INT_MIN;

					if (flatNodes[currentNode->leftChildIndex]->bbox->CheckIntersection(ray, tempMin, tempMax))
					{
						indexesToVisit.push(currentNode->leftChildIndex);
					}

					tempMin = INT_MAX;
					tempMax = INT_MIN;

					if (flatNodes[currentNode->rightChildIndex]->bbox->CheckIntersection(ray, tempMin, tempMax))
					{
						indexesToVisit.push(currentNode->rightChildIndex);
					}
				}
			}
			return testIntersection;
		}

		bool TestIntersectionNormal(Ray ray, ShadeRec& rec, bool p_shadowTest = false)
		{
			double tmin = INT_MAX;
			double tmax = INT_MIN;
			queue<BVHNode*> toVisit;
			
			toVisit.push(rootNode);
			bool testIntersection = false;
			while (toVisit.size() > 0)
			{
				double tempTmin(INT_MAX);
				double tempTmax(INT_MIN);
				BVHNode * currentNode = toVisit.front();
				toVisit.pop();
				
				if (currentNode->childNode)
				{
					for (int objectIndex = 0; objectIndex < currentNode->objects.size(); objectIndex++)
					{
						bool temp = currentNode->objects[objectIndex]->hit(ray.o, ray.d, tmin, rec);
						if (p_shadowTest)
							return true;
						else if (temp)
							testIntersection = temp;
					}
				}
				else 
				{
					if (currentNode->leftChild != NULL && currentNode->leftChild->boundingBox->CheckIntersection(ray, tempTmin, tempTmax))
					{
						toVisit.push(currentNode->leftChild);
					}

					tempTmin = INT_MAX;
					tempTmax = INT_MIN;

					if (currentNode->rightChild != NULL && currentNode->rightChild->boundingBox->CheckIntersection(ray, tempTmin, tempTmax))
					{
						toVisit.push(currentNode->rightChild);
					}
				}
			}
			
			return testIntersection;
		}

		void FlattenBvh(BVHNode* p_currentNode, int& p_currentIndex)
		{
			if (p_currentNode->childNode)
			{
				flatNodes[p_currentIndex] = new FlatBvhNode(p_currentIndex, p_currentNode);
			}
			else
			{
				int currentNodeIndex = p_currentIndex;
				flatNodes[currentNodeIndex] = new FlatBvhNode(p_currentIndex, p_currentNode);
				flatNodes[currentNodeIndex]->leftChildIndex = ++p_currentIndex;
				FlattenBvh(p_currentNode->leftChild, p_currentIndex);
				flatNodes[currentNodeIndex]->rightChildIndex = ++p_currentIndex;
				FlattenBvh(p_currentNode->rightChild, p_currentIndex);
			}
		}

		~BVH()
		{
			/*if (m_flatBvh)
			{
				delete[] flatNodes;
			}*/
		}

	private:
		vector<PrimitiveBvhRepresentation> objectsRepresentation;


	};
}

#endif // !__BVHACCELERATOR__
